package org.tokaf;

import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import org.tokaf.datasearcher.DataModifier;
import org.tokaf.datasearcher.DataSearcher;
import org.tokaf.normalizer.SimpleNormalizer;
import org.tokaf.rater.WeightRater;

/**
 * <p> UserRatings stores information about one user and his ratings. It allows
 * simple way to add, delete or modify users ratings. Also it features method to
 * get users computed ratings using his other ratings. Computed rating of object
 * is calculated from ratings of other individuals, which are connected to the
 * object. </p> <p> Copyright (c) 2006 </p> <p>
 * @author Alan Eckhardt
 * @version 1.0
 */
public class UserRatings {

	@SuppressWarnings("unchecked")
	ArrayList al = new ArrayList();

	String namespace;

	QueryFinder queryFinder;

	DataSearcher master;

	DataModifier masterModifier;

	String typeName = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";

	String userName;

	String ratingName;

	String computedRatingName;

	public UserRatings(String namespace, QueryFinder queryFinder,
			DataSearcher master, DataModifier masterModifier) {
		this(namespace, queryFinder, master, masterModifier, null, null);
	}

	public UserRatings(String namespace, QueryFinder queryFinder,
			DataSearcher master, DataModifier masterModifier,
			String ratingName, String computedRatingName) {
		this.namespace = namespace;
		this.queryFinder = queryFinder;
		this.master = master;
		this.masterModifier = masterModifier;
		this.ratingName = ratingName;
		this.computedRatingName = computedRatingName;
	}

	public String getUserRatingName(String userName, String connectingPredicate) {
		ArrayList<String> params = new ArrayList<String>();
		params.add(checkURI(userName));
		params.add(checkURI(connectingPredicate));

		String ratingsQuery = queryFinder.getQuery("GetListOfEntities", 2,
				params);
		DataSearcher searcher = (DataSearcher) master.clone();

		searcher.initSearch(ratingsQuery);
		if (!searcher.hasNext())
			return null;

		return searcher.getField(2).toString();
	}

	public double getUserRatingOnEntity(String entity) {
		if (entity == null || entity.equals(""))
			return 0;
		ArrayList<String> params = new ArrayList<String>();
		params.add(checkURI(entity));
		params.add(getRating(ratingName));

		String ratingsQuery = queryFinder.getQuery("GetListOfEntities", 2,
				params);
		DataSearcher searcher = (DataSearcher) master.clone();

		double rating = -1;
		searcher.initSearch(ratingsQuery);
		if (!searcher.hasNext())
			return 0;
		// throw new NoSuchElementException("User didn't rate object "+entity);

		// Finds maximum rating from all available ratings.
		while (searcher.hasNext()) {
			Object obj = searcher.getField(2);
			try {
				double tmp = Double.parseDouble(obj.toString());
				if (tmp > rating)
					rating = tmp;
			} catch (Exception ex) {
				ex.printStackTrace();
			}
			searcher.advance();
		}
		return rating;
	}

	public void deleteUserRatingsOnEntity(String entity, boolean computed) {
		ArrayList<String> params = new ArrayList<String>();

		params.add(checkURI(entity));
		if (computed)
			params.add(getRating(computedRatingName));
		else
			params.add(getRating(ratingName));

		String ratingsQuery = queryFinder.getQuery("GetListOfEntities", 2,
				params);
		DataSearcher searcher = (DataSearcher) master.clone();

		searcher.initSearch(ratingsQuery);
		// Deletes all ratings, there should be only one, but one never knows
		while (searcher.hasNext()) {
			Object sub = searcher.getField(0);
			Object pred = searcher.getField(1);
			Object obj = searcher.getField(2);
			masterModifier.deleteTriple(sub, pred, obj);
			searcher.advance();
		}
	}

	/**
	 * Adds rating on entity for current user.
	 * @param entity is name of entity
	 * @param value is the value of rating
	 * @param deletePrevious determines, whether to delete previous ratings
	 * @param computed determines if we use computed rating, or normal rating
	 *            predicate
	 */
	public void addUserRatingsOnEntity(String entity, Object value,
			boolean deletePrevious, boolean computed) {
		if (deletePrevious)
			deleteUserRatingsOnEntity(entity, computed);
		// Adds the new rating
		if (computed)
			masterModifier.addTriple(entity, getRating(computedRatingName),
					value.toString(),
					"http://www.w3.org/2001/10/XMLSchema#decimal");
		else
			masterModifier.addTriple(entity, getRating(ratingName), value
					.toString(), "http://www.w3.org/2001/10/XMLSchema#decimal");
	}

	protected String checkURI(String string) {
		URI a = java.net.URI.create(string);
		if (a.getAuthority() == null)
			return namespace + string;
		else
			return string;
	}

	protected String getRating(String ratingName) {
		try {
			URI a = java.net.URI.create(ratingName);
			if (a.getAuthority() == null) {
				a = java.net.URI.create(namespace + ratingName);
				if (a.getAuthority() == null) {
					IllegalArgumentException newEx = new IllegalArgumentException(
							"Invalid URI " + namespace + ratingName);
					throw newEx;
				}
				return namespace + ratingName;

			} else
				return ratingName;

		} catch (NullPointerException ex) {
			IllegalArgumentException newEx = new IllegalArgumentException(
					"User " + this.userName + " has no rating associated", ex);
			throw newEx;
			// return null;

		}
	}

	public void updateUserRatingsOnEntity(String entity, Object oldValue,
			Object newValue) {
		ArrayList<String> params = new ArrayList<String>();

		params.add(checkURI(entity));
		params.add(getRating(ratingName));
		params.add(checkURI(oldValue.toString()));
		String ratingsQuery = queryFinder.getQuery("GetListOfEntities", 3,
				params);
		DataSearcher searcher = (DataSearcher) master.clone();

		searcher.initSearch(ratingsQuery);
		// Deletes all ratings, there should be only one, but one never knows
		while (searcher.hasNext()) {
			Object sub = searcher.getField(0);
			Object pred = searcher.getField(1);
			Object obj = searcher.getField(2);
			// if((obj.toString()).equals(oldValue.toString()))
			masterModifier.deleteTriple(sub, pred, obj);
			searcher.advance();
		}
		masterModifier.addTriple(entity, getRating(ratingName), newValue
				.toString(), "http://www.w3.org/2001/10/XMLSchema#decimal");
	}

	public HashMap<String, Double> getUserRatingsHashMapOnClass(String className) {
		ArrayList<String> params = new ArrayList<String>();

		params.add(checkURI(className));
		params.add(getRating(ratingName));

		HashMap<String, Double> hm = new HashMap<String, Double>();
		String dataQuery = queryFinder.getQuery("GetListOfTypedEntities", 5,
				params);
		DataSearcher searcher = (DataSearcher) master.clone();

		searcher.initSearch(dataQuery);
		// Finds all ratings
		while (searcher.hasNext()) {
			String name = searcher.getField(0).toString();
			Double d = hm.get(name);
			if (d == null) {
				d = new Double(Double.parseDouble(searcher.getField(2)
						.toString()));
				hm.put(name, d);
			}
			double newD = Double.parseDouble(searcher.getField(2).toString());
			if (d.doubleValue() < newD) {
				hm.remove(name);
				hm.put(name, new Double(newD));
			}
			searcher.advance();
		}
		return hm;
	}

	public ArrayList<TopKElement> getUserRatingsOnClass(String className) {
		ArrayList<String> params = new ArrayList<String>();

		params.add(checkURI(className));
		params.add(getRating(ratingName));

		HashMap<String, TopKElement> hm = new HashMap<String, TopKElement>();
		ArrayList<TopKElement> al = new ArrayList<TopKElement>();
		String dataQuery = queryFinder.getQuery("GetListOfTypedEntities", 5,
				params);
		DataSearcher searcher = (DataSearcher) master.clone();

		searcher.initSearch(dataQuery);
		// Finds all ratings
		while (searcher.hasNext()) {
			String name = searcher.getField(0).toString();
			TopKElement el = hm.get(name);
			if (el == null) {
				el = new TopKElement(name);
				hm.put(name, el);
				al.add(el);
			}
			double d = Double.parseDouble(searcher.getField(2).toString());
			if (el.rating < d)
				el.rating = d;
			searcher.advance();
		}
		return al;
	}

	public void copyUserRatingsOnClass(String className, boolean allRatings) {
		copyUserRatingsOnClass(this.computedRatingName, className, allRatings);
	}

	/**
	 * @param newRating
	 * @param className
	 * @param allRatings specify if for each object copy all ratings or only the
	 *            highest one
	 */
	public void copyUserRatingsOnClass(String newRating, String className,
			boolean allRatings) {
		ArrayList<String> params = new ArrayList<String>();

		params.add(checkURI(className));
		params.add(getRating(ratingName));

		HashMap<String, TopKElement> hm = new HashMap<String, TopKElement>();
		ArrayList<TopKElement> al = new ArrayList<TopKElement>();
		String dataQuery = queryFinder.getQuery("GetListOfTypedEntities", 5,
				params);
		DataSearcher searcher = (DataSearcher) master.clone();

		searcher.initSearch(dataQuery);
		// Finds all ratings
		while (searcher.hasNext()) {
			String name = searcher.getField(0).toString();
			TopKElement el = hm.get(name);
			if (el == null) {
				el = new TopKElement(name);
				hm.put(name, el);
				al.add(el);
			}
			double d = Double.parseDouble(searcher.getField(2).toString());
			if (el.rating < d)
				el.rating = d;
			if (allRatings)
				masterModifier.addTriple(el.name, newRating, new Double(d));
			searcher.advance();
		}
		if (!allRatings) {
			for (int i = 0; i < al.size(); i++) {
				TopKElement el = al.get(i);
				masterModifier.addTriple(el.name, newRating, new Double(
						el.rating).toString());
			}
		}

	}

	protected DataSearcher[] getConnectedRatingSearcher(String objectName,
			String className, String[] predicates) {
		DataSearcher ratedObjects[] = new DataSearcher[predicates.length];
		for (int i = 0; i < predicates.length; i++) {
			ArrayList<String> params = new ArrayList<String>();
			// First argument is the name of class
			params.add(checkURI(className));
			// Second argument is the rating
			params.add(getRating(ratingName));

			// Third argument is the name of entity
			if (objectName == null)
				params.add("");
			else
				params.add(checkURI(objectName));

			// Fourth argument is connecting predicate
			params.add(checkURI(predicates[i]));

			String tripQuery = queryFinder.getQuery("GetListOfSubjectsObjects",
					2, params);
			ratedObjects[i] = (DataSearcher) master.clone();
			ratedObjects[i].initSearch(tripQuery);
		}
		return ratedObjects;
	}

	/**
	 * Return datasearcher, which contains subject, object, rating of subject
	 * and rating of object. It get the triple with predicate <i>predicate</i>.
	 * @param predicate is the name of predicate we are interested in
	 * @param rating is the name of predicate that represents ratings
	 * @return
	 */
	protected DataSearcher getRatedSubjectsAndObjects(String predicate,
			String rating) {
		DataSearcher ratedObjects;
		ArrayList<String> params = new ArrayList<String>();
		// First argument is the name of predicate
		params.add(checkURI(predicate));
		// Second argument is the rating
		params.add(checkURI(rating));
		String tripQuery = queryFinder.getQuery(
				"URGetListOfConnectedEntitiesWithRatings", 1, params);
		ratedObjects = (DataSearcher) master.clone();
		ratedObjects.initSearch(tripQuery);
		return ratedObjects;
	}

	/**
	 * Computes induced rating for all instances of class className.
	 * @param className
	 * @param predicates
	 * @param rater
	 */
	public void setInducedRatings(String className, String[] predicates,
			WeightRater rater) {
		setInducedRatings(className, null, predicates, rater);
	}

	private double computeAverage(ArrayList<Double> al) {
		if (al == null)
			return 0;
		double divider = 0;
		double divisor = 0;
		for (int i = 0; i < al.size(); i++) {
			divisor += al.get(i).doubleValue();
			divider++;
		}
		return divisor / divider;
	}

	/**
	 * Computes induced rating for individual objectName of class className. If
	 * objectName is null, computes it for all individuals of class className.
	 * @param className is class of objectName
	 * @param objectName is the name of object
	 * @param predicates are predicates connecting objectName to complex types
	 * @param rater is the rater that computes final computed rating
	 * @return
	 */
	@SuppressWarnings("unchecked")
	protected HashMap<Object, TopKElement> getInducedRating(String className, String objectName,
			String[] predicates, WeightRater rater) {
		// data is the source of ratings of objects connected to objectName.
		// each data[i] contains name of object objectName, name of connected
		// object O and rating of O.
		DataSearcher[] data = getConnectedRatingSearcher(objectName, className,
				predicates);

		// dataTemp is used only to store SimpleNormalizers
		DataSearcher[] dataTemp = new DataSearcher[predicates.length + 1];

		double[] ratingOnPredicates = new double[predicates.length + 1];
		String[] temp = new String[predicates.length + 1];
		for (int i = 0; i < predicates.length; i++) {
			ratingOnPredicates[i] = getUserRatingOnEntity(predicates[i]);
			dataTemp[i] = (DataSearcher) master.clone();
			temp[i] = predicates[i];
		}

		temp[temp.length - 1] = getRating(ratingName);

		// We add the rating for users own rating of this attribute
		ratingOnPredicates[ratingOnPredicates.length - 1] = 1;
		dataTemp[predicates.length] = (DataSearcher) master.clone();
		
		// We initialize Rater with weights on predicates
		rater.init(temp, ratingOnPredicates);

		HashMap<Object, TopKElement> hm = new HashMap<Object, TopKElement>();
		// For each attribute we will compute
		// 1) for each data[i] overall rating of connected entities, using
		// average
		// 2) aggregate overall ratings into one computed rating.
		for (int i = 0; i < data.length; i++) {
			while (data[i].hasNext()) {
				TopKElement el = hm.get(data[i].getField(0));
				if (el == null) {
					el = new TopKElement(data[i].getField(0),
							predicates.length + 1);
					hm.put(el.name, el);
				}
				if (objectName != null
						&& !objectName.equals(el.name.toString())) {
					data[i].advance();
					continue;
				}
				// We add new rating to object el
				ArrayList<Double> ratingsOfUpper = (ArrayList<Double>) el.getRatingObject(i);
				if (ratingsOfUpper == null)
					ratingsOfUpper = new ArrayList<Double>();
				// We get rating of connected entity, which is on third position
				Object o = data[i].getField(2);
				if (o != null)
					ratingsOfUpper.add(new Double(dataTemp[i].getNormalizer()
							.Normalize(o)));

				el.setRatingObject(i, ratingsOfUpper);

				data[i].advance();
			}
		}

		Iterator<TopKElement> i = hm.values().iterator();
		while (i.hasNext()) {
			TopKElement el = i.next();
			// For each i, we compute average of ratings of entities, which are
			// connected by predicates[i]
			double ratings[] = new double[data.length];
			for (int j = 0; j < ratings.length; j++)
				if (!el.isNull(j))
					el.setRating(j, new Double(computeAverage((ArrayList<Double>) el
							.getRatingObject(j))), dataTemp[j].getNormalizer());
			// Finally, we add rating of entity itself
			el.setRating(el.getLength() - 1, new Double(
					getUserRatingOnEntity(el.name.toString())), dataTemp[el
					.getLength() - 1].getNormalizer());
			rater.rate(el, dataTemp);
		}
		return hm;
	}

	/**
	 * Computes rating of one individual objectName.
	 * @param className
	 * @param objectName
	 * @param predicates
	 * @param rater
	 * @return double
	 */
	public double getInducedRatingOnEntity(String className, String objectName,
			String[] predicates, WeightRater rater) {
		if (objectName == null)
			return 0;
		HashMap<Object, TopKElement> hm = getInducedRating(className, objectName, predicates, rater);
		Iterator<TopKElement> i = hm.values().iterator();
		while (i.hasNext()) {
			TopKElement el = i.next();
			if (el.name.toString().equals(objectName))
				return el.rating;
		}
		return 0;
	}

	@SuppressWarnings("unchecked")
	protected HashMap<Object, TopKElement> getHashMapRatingsOnPredicate(String predicate) {
		DataSearcher data = getRatedSubjectsAndObjects(predicate,
				this.computedRatingName);
		HashMap<Object, TopKElement> attributes = new HashMap<Object, TopKElement>();
		SimpleNormalizer norm = new SimpleNormalizer();
		while (data.hasNext()) {
			// We get the name of attribute
			TopKElement el = attributes.get(data.getField(2));
			if (el == null) {
				el = new TopKElement(data.getField(2), 1);
				attributes.put(el.name, el);
			}
			double rating = norm.Normalize(data.getField(3));
			if (rating > el.rating)
				el.rating = rating;

			// We add new rating to attribute el
			ArrayList<Double> ratingsOfUpper = (ArrayList<Double>) el.getRatingObject(0);
			if (ratingsOfUpper == null)
				ratingsOfUpper = new ArrayList<Double>();
			// We get rating of subject, which is on second position
			Object o = data.getField(1);
			if (o != null)
				ratingsOfUpper.add(new Double(norm.Normalize(o)));
			el.setRatingObject(0, ratingsOfUpper);
			data.advance();
		}
		return attributes;
	}

	/**
	 * Computes induced of predicate. It is the correlation between ratings of
	 * subject and object of predicate.
	 * @param predicate is the name of predicate.
	 * @return double
	 */
	@SuppressWarnings("unchecked")
	public double getInducedRatingOnPredicate(String predicate) {
		HashMap<Object, TopKElement> attributes = getHashMapRatingsOnPredicate(predicate);
		ArrayList<TopKElement> al = new ArrayList<TopKElement>(attributes.values());
		double correlations = 0;
		for (int i = 0; i < al.size(); i++) {
			TopKElement el = al.get(i);
			double avg = this.computeAverage((ArrayList<Double>) el.getRatingObject(0));
			double rating = el.rating;
			correlations += (Math.abs(avg - rating));
		}
		return correlations / al.size();
	}

	public void setInducedRatingOnPredicate(String predicate) {
		double rating = getInducedRatingOnPredicate(predicate);
		this.addUserRatingsOnEntity(predicate, new Double(rating), true, true);
	}

	/**
	 * Sets induced rating for individual objectName of class className and
	 * stores it to database.
	 * @param className
	 * @param objectName
	 * @param predicates
	 * @param rater
	 */
	public void setInducedRatings(String className, String objectName,
			String[] predicates, WeightRater rater) {

		HashMap<Object, TopKElement> hm = getInducedRating(className, objectName, predicates, rater);
		Iterator<TopKElement> i = hm.values().iterator();
		while (i.hasNext()) {
			TopKElement el = i.next();
			// We add induced ratings and will use computed ratings
			this.addUserRatingsOnEntity(el.name.toString(), new Double(
					el.rating), true, true);
		}
	}

	public String getRatingName() {
		return ratingName;
	}

	public String getComputedRatingName() {
		return computedRatingName;
	}

	public void setRatingNames(String ratingName, String computedRatingName) {
		this.ratingName = ratingName;
		this.computedRatingName = computedRatingName;
	}

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
		computedRatingName = getUserRatingName(userName,
				RatingsContainer.computedRatingPredicate);
		ratingName = getUserRatingName(userName,
				RatingsContainer.ratingPredicate);

	}
}
